import { RoomEnvironment } from 'three/examples/jsm/Addons.js';
import {
  AnimationMixer,
  Bone,
  type BufferGeometry,
  Camera,
  CameraHelper,
  DirectionalLight,
  DirectionalLightHelper,
  HemisphereLight,
  HemisphereLightHelper,
  type Material,
  Mesh,
  MeshBasicMaterial,
  type Object3D,
  PMREMGenerator,
  PerspectiveCamera,
  PointLight,
  PointLightHelper,
  Scene,
  SkeletonHelper,
  SkinnedMesh,
  SphereGeometry,
  SpotLight,
  SpotLightHelper,
  type Texture,
  Vector3,
  WebGPURenderer,
} from 'three/webgpu';

import type { Command } from './commands/Command.ts';
import { config } from './Editor.config.ts';
import { signals } from './Editor.signals.ts';
import { storage } from './Editor.storage.ts';
import { utils } from './Editor.utils.ts';
import { History } from './History.ts';
import { Loader } from './loader/Loader.ts';
import { Selector } from './Selector.ts';
import { initViewport } from './Viewport.ts';

const _DEFAULT_CAMERA = new PerspectiveCamera(50, 1, 1, 1000);
_DEFAULT_CAMERA.name = 'Camera';
_DEFAULT_CAMERA.position.set(0, 5, 10);
_DEFAULT_CAMERA.lookAt(new Vector3());

type Helper =
  | CameraHelper
  | SkeletonHelper
  | PointLightHelper
  | DirectionalLightHelper
  | SpotLightHelper
  | HemisphereLightHelper;

const geometry = new SphereGeometry(2, 4, 2);
const material = new MeshBasicMaterial({ color: 0xff0000, visible: false });

export class Editor {
  public static instance: Editor | undefined;

  public targetElement!: HTMLElement;

  public viewportShading: string = 'default';

  public signals!: typeof signals;
  public utils!: typeof utils;
  public config!: typeof config;
  public storage!: typeof storage;

  public selector!: Selector;
  public history!: History;
  public loader!: Loader;

  public camera!: Camera;
  public scene!: Scene;
  public renderer!: WebGPURenderer;
  public sceneHelpers!: Scene;
  public viewportCamera!: Camera;
  public pmremGenerator!: PMREMGenerator;

  public objects: Map<string, Object3D> = new Map();
  public geometries: Map<string, BufferGeometry> = new Map();
  public materials: Map<string, Material> = new Map();
  public textures: Map<string, Texture> = new Map();
  public materialsRefCounter: Map<Material, number> = new Map(); // tracks how often is a material used by a 3D object

  public helpers: Map<number, Helper> = new Map();
  public cameras: Map<string, Camera> = new Map();

  public mixer!: AnimationMixer;
  public selected!: Object3D | null;

  constructor(targetElement: HTMLElement) {
    if (Editor.instance) {
      return Editor.instance;
    }
    if (!targetElement) {
      return;
    }

    Editor.instance = this;

    this.targetElement = targetElement;

    this.signals = signals;
    this.utils = utils;
    this.config = config;
    this.storage = storage;

    this.history = new History(this);
    this.selector = new Selector(this);
    this.loader = new Loader(this);

    this.camera = _DEFAULT_CAMERA.clone();
    this.viewportCamera = this.camera;

    this.scene = new Scene();
    this.scene.name = 'Scene';

    this.sceneHelpers = new Scene();
    this.sceneHelpers.add(new HemisphereLight(0xffffff, 0x888888, 2));

    this.mixer = new AnimationMixer(this.scene);

    this.selected = null;

    this.geometries = new Map();
    this.materials = new Map();
    this.textures = new Map();

    this.materialsRefCounter = new Map(); // tracks how often is a material used by a 3D object

    this.helpers = new Map();
    this.cameras = new Map();

    initViewport(this);

    this.addCamera(this.camera);

    this.renderer = this.createRenderer();

    this.pmremGenerator = new PMREMGenerator(this.renderer);
    this.pmremGenerator.fromSceneAsync(new RoomEnvironment(), 0.04).then((renderTarget) => {
      this.scene.environment = renderTarget.texture;
    });
  }

  createRenderer() {
    if (this.renderer) {
      this.renderer.setAnimationLoop(null);
      this.renderer.dispose();
      this.pmremGenerator.dispose();
      this.targetElement.removeChild(this.renderer.domElement);
    }

    this.renderer = new WebGPURenderer({ antialias: true, logarithmicDepthBuffer: true });
    this.renderer.setPixelRatio(window.devicePixelRatio);

    this.renderer.setSize(this.targetElement.clientWidth, this.targetElement.clientHeight);
    this.targetElement.appendChild(this.renderer.domElement);

    this.pmremGenerator = new PMREMGenerator(this.renderer);

    signals.rendererCreated.dispatch(this.renderer);
    signals.rendererUpdated.dispatch();

    return this.renderer;
  }

  addObject = (object: Object3D, parent?: Object3D, index?: number) => {
    object.traverse((child) => {
      if ((child as Mesh).geometry !== undefined) this.addGeometry((child as Mesh).geometry);
      if ((child as Mesh).material !== undefined) this.addMaterial((child as Mesh).material);

      this.addCamera(child as Camera);
      this.addHelper(child);
    });

    if (parent === undefined) {
      this.scene.add(object);
    } else if (index === undefined) {
      parent.children.push(object);
      object.parent = parent;
    } else {
      parent.children.splice(index, 0, object);
      object.parent = parent;
    }

    this.signals.objectAdded.dispatch(object);
    this.signals.sceneGraphChanged.dispatch();
  };

  removeObject = (object: Object3D) => {
    const _parent = object.parent;

    if (_parent === null) return; // avoid deleting the camera or scene

    object.traverse((child) => {
      this.removeCamera(child as Camera);
      this.removeHelper(child as Helper);
      if ((child as Mesh).material !== undefined) this.removeMaterial((child as Mesh).material);
    });

    _parent.remove(object);

    this.signals.objectRemoved.dispatch({ object, parent: _parent });
    this.signals.sceneGraphChanged.dispatch();
  };

  addGeometry = (geometry: BufferGeometry) => {
    this.geometries.set(geometry.uuid, geometry);
  };

  addMaterial = (material: Material | Material[]) => {
    if (Array.isArray(material)) {
      for (let i = 0, l = material.length; i < l; i++) {
        this.addMaterialToRefCounter(material[i]);
      }
    } else {
      this.addMaterialToRefCounter(material);
    }
    this.signals.materialAdded.dispatch();
  };

  addMaterialToRefCounter = (material: Material) => {
    const materialsRefCounter = this.materialsRefCounter;

    if (materialsRefCounter.has(material)) {
      const count = materialsRefCounter.get(material) as number;
      materialsRefCounter.set(material, count);
    } else {
      materialsRefCounter.set(material, 1);
      this.materials.set(material.uuid, material);
    }
  };
  removeMaterial = (_material: Material | Material[]) => {};
  addHelper = (object: Object3D, helper?: Helper) => {
    if (helper === undefined) {
      if (object instanceof Camera) {
        helper = new CameraHelper(object);
      } else if (object instanceof PointLight) {
        helper = new PointLightHelper(object, 1);
      } else if (object instanceof DirectionalLight) {
        helper = new DirectionalLightHelper(object, 1);
      } else if (object instanceof SpotLight) {
        helper = new SpotLightHelper(object);
      } else if (object instanceof HemisphereLight) {
        helper = new HemisphereLightHelper(object, 1);
      } else if (object instanceof SkinnedMesh) {
        helper = new SkeletonHelper(object.skeleton.bones[0]);
      } else if (object instanceof Bone && object.parent && !(object.parent instanceof Bone)) {
        helper = new SkeletonHelper(object);
      } else {
        return;
      }

      const picker = new Mesh(geometry, material);
      picker.name = 'picker';
      picker.userData.object = object;
      helper.add(picker);
    }

    this.sceneHelpers.add(helper);

    this.helpers.set(object.id, helper);
    this.signals.helperAdded.dispatch(helper);
  };
  removeHelper = <T extends Helper>(object: T) => {
    if (this.helpers.has(object.id)) {
      const helper = this.helpers.get(object.id) as T;
      helper.parent?.remove(helper);
      helper.dispose();
      this.helpers.delete(object.id);
      this.signals.helperRemoved.dispatch(helper);
    }
  };
  addCamera = (camera: Camera) => {
    if (camera.isCamera) {
      this.cameras.set(camera.uuid, camera);
      this.signals.cameraAdded.dispatch(camera);
    }
  };

  removeCamera = (camera: Camera) => {
    if (this.cameras.has(camera.uuid)) {
      this.cameras.delete(camera.uuid);
      this.signals.cameraRemoved.dispatch(camera);
    }
  };

  select = (object?: Object3D) => {
    if (object) {
      this.selector.select(object);
    }
  };

  selectById = (id: number) => {
    if (id === this.camera.id) {
      this.select(this.camera);
      return;
    }
    this.select(this.scene.getObjectById(id));
  };

  selectByUuid = (uuid: string) => {
    this.scene.traverse((child) => {
      if (child.uuid === uuid) {
        this.select(child);
      }
    });
  };

  deselect = () => {
    this.selector.deselect();
  };

  focus = (object?: Object3D) => {
    if (object) {
      this.signals.objectFocused.dispatch(object);
    }
  };

  focusById = (id: number) => {
    this.focus(this.scene.getObjectById(id));
  };

  execute = (command: Command) => {
    this.history.execute(command);
  };

  redo() {}

  undo() {}
}
